123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389 |
- <template>
- <div>
- <div class="w-full h-[55px] sm:h-[72px]"></div>
- <ErrorBoundary :error="error">
- <div v-if="isLoading" class="flex justify-center py-12">
- <!-- 加载中 -->
- <div
- class="animate-spin h-8 w-8 border-4 border-cyan-400 rounded-full border-t-transparent"
- ></div>
- </div>
-
- <div v-else>
- <!-- 面包屑导航 -->
- <div class="max-w-full mb-6 xl:px-2 lg:px-2 md:px-4 px-4 mt-6">
- <div class="max-w-screen-2xl mx-auto">
- <nuxt-link
- :to="`${homepagePath}/`"
- class="justify-start text-white/60 text-base font-normal"
- >{{ t("common.breadcrumb.home") }}</nuxt-link
- >
- <span class="text-white/60 text-base font-normal px-2"> / </span>
- <nuxt-link
- v-if="product?.category"
- :to="`${homepagePath}/products?audiences=${product.meta?.audiences}`"
- class="text-white/60 text-base font-normal"
- >{{
- product.meta?.audiences === 0
- ? t("common.personal")
- : t("common.business")
- }}</nuxt-link
- >
- <span class="text-white/60 text-base font-normal px-2"> / </span>
- <nuxt-link
- v-if="product?.category"
- :to="`${homepagePath}/products?category=${encodeURIComponent(
- product.category
- )}&audiences=${product.meta?.audiences}`"
- class="text-white/60 text-base font-normal"
- >{{ product.category }}</nuxt-link
- >
- <span class="text-white/60 text-base font-normal px-2"> / </span>
- <span class="text-white text-base font-normal">{{
- product?.title || product?.name
- }}</span>
- </div>
- </div>
-
- <!-- 产品详情内容 -->
- <div
- v-if="product"
- class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4"
- >
- <div class="max-w-screen-2xl mx-auto">
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16">
- <!-- 左侧产品图片 -->
- <div
- class="flex flex-col gap-6 lg:sticky lg:top-24 self-start select-none"
- >
- <!-- 主图展示 -->
- <div
- class="bg-zinc-900 rounded-lg box-border relative overflow-hidden group aspect-square"
- >
- <!-- 主图容器 - Swiper -->
- <Swiper
- :modules="[Navigation, Pagination, EffectFade, Thumbs]"
- :slides-per-view="1"
- :pagination="{
- clickable: true,
- dynamicBullets: true,
- dynamicMainBullets: 3,
- }"
- :navigation="true"
- :effect="'fade'"
- :fade-effect="{ crossFade: true }"
- :thumbs="{ swiper: thumbsSwiper }"
- class="w-full h-full product-swiper"
- @swiper="swiperInstance = $event"
- @slideChange="currentSlideIndex = $event.activeIndex"
- >
- <SwiperSlide
- v-for="(image, slideIndex) in [
- product.image,
- ...(product.gallery || []),
- ]"
- :key="slideIndex"
- >
- <div class="relative w-full h-full">
- <!-- 当前图片 -->
- <img
- :src="image"
- :alt="`${product.name} - ${t('products.image', {
- index: slideIndex + 1,
- })}`"
- class="w-full h-full object-contain rounded-lg transition-all duration-500"
- :class="{
- 'opacity-0': isSlideThumbnailLoading[slideIndex],
- 'opacity-100':
- !isSlideThumbnailLoading[slideIndex] &&
- !slideThumbnailErrors[slideIndex],
- }"
- @load="handleSlideImageLoad(slideIndex)"
- @error="handleSlideImageError(slideIndex)"
- />
-
- <!-- 加载状态 -->
- <div
- v-if="isSlideThumbnailLoading[slideIndex]"
- class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 z-10"
- >
- <div
- class="animate-spin h-8 w-8 border-4 border-cyan-400 rounded-full border-t-transparent"
- ></div>
- </div>
-
- <!-- 错误提示 -->
- <div
- v-if="slideThumbnailErrors[slideIndex]"
- class="absolute inset-0 flex items-center justify-center bg-red-900/50 z-20"
- >
- <div class="flex flex-col items-center gap-2">
- <span class="text-white">{{
- t("products.imageLoadError")
- }}</span>
- <button
- @click.stop="retryLoadSlideImage(slideIndex)"
- class="px-4 py-2 bg-cyan-400 text-white rounded-lg hover:bg-cyan-600 transition-colors duration-300"
- >
- {{ t("products.retry") }}
- </button>
- </div>
- </div>
- </div>
- </SwiperSlide>
- </Swiper>
-
- <!-- 全局加载状态 - 仅在所有图片都未加载完成时显示 -->
- <div
- v-if="isInitialLoading"
- class="absolute inset-0 flex items-center justify-center bg-zinc-900/80 z-30"
- >
- <div
- class="animate-spin h-12 w-12 border-4 border-cyan-400 rounded-full border-t-transparent"
- ></div>
- </div>
- </div>
-
- <!-- 缩略图列表 - Swiper -->
- <div
- class="relative bg-zinc-900 rounded-lg py-6 px-8 overflow-hidden product-thumbnail-container"
- >
- <Swiper
- :modules="[Navigation, Thumbs, FreeMode]"
- :slides-per-view="'auto'"
- :space-between="12"
- :free-mode="true"
- :watch-slides-progress="true"
- :navigation="{
- nextEl: '.swiper-thumb-next',
- prevEl: '.swiper-thumb-prev',
- }"
- class="thumbs-swiper"
- @swiper="thumbsSwiper = $event"
- >
- <SwiperSlide
- v-for="(image, index) in [
- product.image,
- ...(product.gallery || []),
- ]"
- :key="index"
- class="!w-16 !h-16 md:!w-20 md:!h-20 !flex-shrink-0 cursor-pointer transition-all duration-300 relative"
- >
- <div
- class="w-full h-full rounded-lg overflow-hidden thumbnail-fixed-size"
- :class="{
- 'ring-2 ring-cyan-400 ring-offset-2 ring-offset-zinc-900':
- currentSlideIndex === index,
- 'hover:ring-1 hover:ring-cyan-400/50 hover:ring-offset-1 hover:ring-offset-zinc-900':
- currentSlideIndex !== index,
- 'opacity-50':
- isSlideThumbnailLoading[index] ||
- slideThumbnailErrors[index],
- }"
- >
- <!-- 缩略图加载状态 -->
- <div
- v-if="isSlideThumbnailLoading[index]"
- class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 rounded-lg z-10"
- >
- <div
- class="animate-spin h-4 w-4 border-2 border-cyan-400 rounded-full border-t-transparent"
- ></div>
- </div>
-
- <!-- 缩略图遮罩 -->
- <div
- class="absolute inset-0 transition-all duration-300 rounded-lg"
- :class="{
- 'bg-black/30': currentSlideIndex === index,
- 'bg-black/0 hover:bg-black/20':
- currentSlideIndex !== index,
- }"
- ></div>
-
- <img
- :src="image"
- :alt="`${product.name} - ${t('products.image', {
- index: index + 1,
- })}`"
- class="w-full h-full object-cover transition-all duration-300 rounded-lg"
- :class="{
- 'opacity-0': isSlideThumbnailLoading[index],
- 'opacity-100':
- !isSlideThumbnailLoading[index] &&
- !slideThumbnailErrors[index],
- 'hover:scale-110': currentSlideIndex !== index,
- }"
- @load="handleSlideImageLoad(index)"
- @error="handleSlideImageError(index)"
- />
-
- <!-- 缩略图错误提示 -->
- <div
- v-if="slideThumbnailErrors[index]"
- class="absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg"
- >
- <div class="flex flex-col items-center gap-1">
- <span class="text-white text-xs">{{
- t("products.error")
- }}</span>
- <button
- @click.stop="retryLoadSlideImage(index)"
- class="px-2 py-1 bg-cyan-400 text-white text-xs rounded hover:bg-cyan-600 transition-colors duration-300"
- >
- {{ t("products.retry") }}
- </button>
- </div>
- </div>
- </div>
- </SwiperSlide>
- </Swiper>
-
- <!-- 缩略图导航按钮 -->
- <button
- class="swiper-thumb-prev absolute top-1/2 left-2 z-10 w-8 h-8 flex items-center justify-center bg-black/50 hover:bg-cyan-400 rounded-full transform -translate-y-1/2 transition-all duration-300"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-5 w-5 text-white"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M15 19l-7-7 7-7"
- />
- </svg>
- </button>
- <button
- class="swiper-thumb-next absolute top-1/2 right-2 z-10 w-8 h-8 flex items-center justify-center bg-black/50 hover:bg-cyan-400 rounded-full transform -translate-y-1/2 transition-all duration-300"
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- class="h-5 w-5 text-white"
- fill="none"
- viewBox="0 0 24 24"
- stroke="currentColor"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- stroke-width="2"
- d="M9 5l7 7-7 7"
- />
- </svg>
- </button>
- </div>
- </div>
-
- <!-- 右侧产品信息 -->
- <div class="flex flex-col gap-8">
- <!-- 产品名称 -->
- <div class="bg-zinc-900 rounded-lg p-2 md:p-6">
- <h1 class="text-white text-3xl font-medium mb-4">
- {{ product.title || product.name }}
- </h1>
- <div
- class="text-[#71717A] text-sm md:text-base leading-relaxed"
- >
- {{ product.summary }}
- </div>
- </div>
-
- <!-- 产品参数 -->
- <!-- <div
- class="bg-zinc-900 rounded-lg p-2 md:p-6 text-sm md:text-base"
- >
- <div class="grid grid-cols-1 gap-4">
- <div
- class="flex justify-between items-center gap-2 py-2 border-b border-zinc-800"
- >
- <span class="text-[#71717A] whitespace-nowrap">{{
- t("products.modelTitle")
- }}</span>
- <span class="text-white font-medium">{{
- product.model
- }}</span>
- </div>
- <div
- class="flex justify-between items-center gap-2 py-2 border-b border-zinc-800"
- >
- <span class="text-[#71717A] whitespace-nowrap">{{
- t("products.categoryTitle")
- }}</span>
- <span class="text-white font-medium"
- >{{ product.category
- }}<template v-if="product.tag">
- / {{ product.tag }}</template
- ></span
- >
- </div>
- <div
- v-if="product.tag"
- class="flex justify-between items-center gap-2 py-2 border-b border-zinc-800"
- >
- <span class="text-[#71717A] whitespace-nowrap">{{
- t("products.seriesTitle")
- }}</span>
- <span class="text-white font-medium">{{
- product.series?.join(" / ")
- }}</span>
- </div>
- <div
- class="flex justify-between items-center gap-2 py-2 border-b border-zinc-800"
- >
- <span class="text-[#71717A] whitespace-nowrap">{{
- t("products.usageTitle")
- }}</span>
- <span class="text-white font-medium">{{
- product.usage?.join(", ")
- }}</span>
- </div>
- <div
- v-if="product.capacities && product.capacities.length > 0"
- class="flex justify-between items-center py-2"
- >
- <span class="text-[#71717A]">{{
- t("products.capacitiesTitle")
- }}</span>
- <span class="text-white font-medium">{{
- product.capacities?.join(" / ")
- }}</span>
- </div>
- </div>
- </div> -->
-
- <!-- 产品描述 -->
- <!-- <div
- v-if="product.description"
- class="bg-zinc-900 rounded-lg p-2 md:p-6"
- >
- <h2 class="text-white text-xl font-medium mb-2 md:mb-6">
- {{ t("products.productDescription") }}
- </h2>
- <div
- class="text-[#71717A] leading-relaxed space-y-4 prose prose-invert max-w-none text-sm md:text-base"
- >
- {{ product.description }}
- </div>
- </div> -->
-
- <!-- 个人用户产品描述 -->
- <div
- v-if="product.meta?.audiences === 0"
- class="bg-zinc-900 rounded-lg p-2 md:p-6"
- >
- <div
- class="text-[#71717A] leading-relaxed space-y-4 prose prose-invert max-w-none text-sm md:text-base"
- >
- <ContentRenderer :value="product.content" />
- </div>
- </div>
-
- <!-- 相关产品 -->
- <div
- v-if="relatedProducts.length > 0"
- class="bg-zinc-900 rounded-lg p-2 md:p-6"
- >
- <h2 class="text-white text-xl font-medium mb-2 md:mb-6">
- {{ t("products.relatedProducts") }}
- </h2>
- <div class="grid grid-cols-2 md:grid-cols-3 gap-4">
- <nuxt-link
- v-for="relatedProduct in relatedProducts"
- :key="relatedProduct.id"
- :to="`${homepagePath}/products/${relatedProduct.id}`"
- class="group"
- >
- <div
- class="bg-zinc-800 rounded-lg p-4 transition-all duration-300 hover:bg-zinc-700"
- >
- <div
- class="aspect-square mb-4 overflow-hidden rounded-lg"
- >
- <img
- :src="relatedProduct.image"
- :alt="relatedProduct.title || relatedProduct.name"
- class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
- />
- </div>
- <h3
- class="text-white text-base md:text-lg font-medium mb-2 line-clamp-2"
- >
- {{ relatedProduct.title || relatedProduct.name }}
- </h3>
- <p class="text-[#71717A] text-sm line-clamp-2">
- {{ relatedProduct.summary }}
- </p>
- </div>
- </nuxt-link>
- </div>
- </div>
- </div>
- </div>
- <!-- 企业用户产品描述 -->
- <div
- v-if="product.meta?.audiences === 1"
- class="bg-zinc-900 rounded-lg p-2 md:p-6 mt-6 full-table-screen"
- >
- <div
- class="text-[#71717A] leading-relaxed space-y-4 prose prose-invert max-w-none text-sm md:text-base"
- >
- <ContentRenderer
- :value="product.content"
- />
- </div>
- </div>
- </div>
- </div>
- </div>
- </ErrorBoundary>
- </div>
- </template>
-
- <script setup lang="ts">
- /**
- * 产品详情页面
- * 展示产品主图、参数和描述
- */
- import { useErrorHandler } from "~/composables/useErrorHandler";
- import { useTableHighlight } from "~/composables/useTableHighlight";
- import { useRoute, useI18n, useAsyncData } from "#imports";
- import { queryCollection } from "#imports";
- import { ContentRenderer } from "#components";
- import { Swiper, SwiperSlide } from "swiper/vue";
- import {
- Navigation,
- Pagination,
- EffectFade,
- Thumbs,
- FreeMode,
- } from "swiper/modules";
- import type { Swiper as SwiperType } from "swiper";
- import "swiper/css";
- import "swiper/css/navigation";
- import "swiper/css/pagination";
- import "swiper/css/effect-fade";
-
- const { error, isLoading } = useErrorHandler();
- const { initTableHighlight } = useTableHighlight();
- const route = useRoute();
- const { locale, t } = useI18n();
- const id = route.params.id as string;
- const swiperInstance = ref<SwiperType | null>(null);
- const thumbsSwiper = ref<SwiperType | null>(null);
-
- // 图片状态
- const currentSlideIndex = ref(0);
- const isInitialLoading = ref(true); // 初始加载状态
- const isSlideThumbnailLoading = ref<boolean[]>([]);
- const slideThumbnailErrors = ref<boolean[]>([]);
-
- // 滚动跟随相关
- const scrollContainer = ref<HTMLElement | null>(null);
- const isSticky = ref(false);
-
- const homepagePath = computed(() => {
- return locale.value === "zh" ? "" : `/${locale.value}`;
- });
-
- interface Product {
- id: string;
- name: string;
- model: string;
- usage: string[];
- capacities: string[];
- category: string;
- categoryId: string;
- description: string;
- summary: string;
- image: string;
- gallery: string[];
- body: string;
- content?: any;
- tag?: string;
- series?: string[];
- meta?: {
- series?: string[];
- name?: string;
- title?: string;
- image?: string;
- summary?: string;
- audiences: number;
- };
- title?: string;
- }
-
- /**
- * 使用queryCollection获取产品数据
- */
- const { data: productContent } = await useAsyncData(
- `product-${id}`,
- async () => {
- try {
- // 使用queryCollection从content目录获取数据
- const content = await queryCollection("content")
- .where("path", "LIKE", `/products/${locale.value}/${id}`)
- .first();
- return content;
- } catch (err) {
- console.error("Error fetching product content:", err);
- error.value = new Error(t("products.loadError"));
- return null;
- }
- }
- );
-
- /**
- * 获取分类信息
- */
-
- const { data: categoryContent } = await useAsyncData(
- `category-${productContent.value?.meta?.categoryId}`,
- async () => {
- if (!productContent.value?.meta?.categoryId) return null;
- try {
- const content = await queryCollection("content")
- .where(
- "path",
- "LIKE",
- `/categories/${locale.value}/${productContent.value.meta?.categoryId}`
- )
- .first();
- return content;
- } catch (err) {
- console.error("Error fetching category:", err);
- return null;
- }
- },
- {
- immediate: !!productContent.value?.meta?.categoryId,
- }
- );
-
- /**
- * 使用计算属性解析产品数据
- */
- const product = computed<Product | any>(() => {
- if (!productContent.value) return null;
-
- // 提取产品数据
- const meta = productContent.value.meta || {};
-
- return {
- id: id,
- name: String(meta.name || productContent.value.title || ""),
- model: String(meta.model || ""),
- title: String(productContent.value.title || meta.name || ""),
- usage: Array.isArray(meta.usage) ? meta.usage : [],
- capacities: Array.isArray(meta.capacities) ? meta.capacities : [],
- category: categoryContent.value?.title || "",
- categoryId: meta.categoryId || "",
- description: productContent.value.description || "",
- summary: String(meta.summary || ""),
- image: String(meta.image || ""),
- gallery: Array.isArray(meta.gallery) ? meta.gallery : [],
- body: productContent.value.body || "",
- content: productContent.value,
- tag: meta.tag || "",
- series: Array.isArray(meta.series) ? meta.series : [],
- sn: meta.sn || "",
- meta: {
- series: Array.isArray(meta.series) ? meta.series : [],
- name: String(meta.name || ""),
- title: String(productContent.value.title || ""),
- image: String(meta.image || ""),
- summary: String(meta.summary || ""),
- audiences: categoryContent.value?.meta?.audiences || 0,
- sn: meta.sn || "",
- },
- };
- });
-
- // 初始化表格高亮功能
- initTableHighlight(computed(() => product.value?.model || ""));
-
- /**
- * 获取相关产品
- */
- const { data: relatedProductsContent } = await useAsyncData(
- `related-products-${id}`,
- async () => {
- try {
- // 获取产品列表
- const content = await queryCollection("content")
- .where("path", "LIKE", `/products/${locale.value}/%`)
- .all();
-
- console.log(content);
-
- const relatedProducts = content.filter((item: any) => {
- const meta = item.meta || {};
- return meta.sn === product.value.meta?.sn;
- });
-
- return relatedProducts;
- } catch (err) {
- console.error("Error fetching related products:", err);
- return [];
- }
- }
- );
-
- /**
- * 处理相关产品数据
- */
- const relatedProducts = computed(() => {
- if (!relatedProductsContent.value || !product.value) return [];
-
- // 获取当前产品的分类和系列
- const currentCategory = product.value.categoryId;
- const currentSeries = product.value.meta?.series || [];
- const currentProductId = id;
-
- return relatedProductsContent.value
- .filter((item: any) => {
- // 排除当前产品 - 多重检查确保排除
- if (item._path === `/products/${locale.value}/${id}`) return false;
-
- const meta = item.meta || {};
- if (meta.name === currentProductId) return false;
-
- const itemCategoryId = meta.categoryId || "";
- const itemSeries = Array.isArray(meta.series) ? meta.series : [];
-
- // 判断是否同类别或同系列
- const isSameCategory =
- currentCategory && itemCategoryId === currentCategory;
- const hasSameSeries =
- currentSeries.length > 0 &&
- itemSeries.some((series: string) => currentSeries.includes(series));
-
- // 返回同类别或同系列的产品
- return isSameCategory || hasSameSeries;
- })
- .map((item: any) => {
- const meta = item.meta || {};
- return {
- id: meta.name || "",
- name: meta.name || item.title || "",
- title: item.title || meta.name || "",
- image: meta.image || "",
- summary: meta.summary || "",
- category: meta.categoryId || "",
- series: Array.isArray(meta.series) ? meta.series : [],
- };
- })
- .slice(0, 6); // 最多显示6个相关产品
- });
-
- /**
- * 预加载下一张图片
- */
- function preloadNextImage(image: string) {
- currentSlideIndex.value =
- (currentSlideIndex.value + 1) % (product.value?.gallery?.length || 1);
- }
-
- /**
- * 处理图片加载完成
- */
- function handleSlideImageLoad(index: number) {
- isSlideThumbnailLoading.value[index] = false;
- slideThumbnailErrors.value[index] = false;
-
- // 检查是否所有图片都已加载
- const allImagesLoaded = isSlideThumbnailLoading.value.every(
- (status) => !status
- );
- if (allImagesLoaded) {
- isInitialLoading.value = false;
- }
- }
-
- /**
- * 处理图片加载错误
- */
- function handleSlideImageError(index: number) {
- isSlideThumbnailLoading.value[index] = false;
- slideThumbnailErrors.value[index] = true;
- }
-
- /**
- * 重试加载图片
- */
- function retryLoadSlideImage(index: number) {
- isSlideThumbnailLoading.value[index] = true;
- slideThumbnailErrors.value[index] = false;
-
- // 确定正确的图片URL
- const images = [product.value?.image, ...(product.value?.gallery || [])];
- const imageUrl = images[index];
-
- // 检查图片URL是否有效
- if (!imageUrl) {
- console.error("Invalid image URL:", index);
- slideThumbnailErrors.value[index] = true;
- isSlideThumbnailLoading.value[index] = false;
- return;
- }
-
- // 创建新的图片对象并设置超时
- const img = new Image();
- const timeoutId = setTimeout(() => {
- handleSlideImageError(index);
- }, 10000); // 10秒超时
-
- img.onload = () => {
- clearTimeout(timeoutId);
- handleSlideImageLoad(index);
- };
-
- img.onerror = (error) => {
- clearTimeout(timeoutId);
- console.error("Image load error:", { index, error });
- handleSlideImageError(index);
- };
-
- // 设置跨域属性
- img.crossOrigin = "anonymous";
-
- // 最后设置src以开始加载
- img.src = imageUrl;
- }
-
- // 页面加载时初始化状态
- onMounted(() => {
- // 初始化缩略图加载状态数组
- const galleryLength = (product.value?.gallery?.length || 0) + 1;
- isSlideThumbnailLoading.value = new Array(galleryLength).fill(true);
- slideThumbnailErrors.value = new Array(galleryLength).fill(false);
-
- // 设置初始加载状态
- isInitialLoading.value = true;
-
- // 预加载所有缩略图
- const images = [product.value?.image, ...(product.value?.gallery || [])];
- images.forEach((image, index) => {
- if (image) {
- const img = new Image();
- img.onload = () => handleSlideImageLoad(index);
- img.onerror = () => handleSlideImageError(index);
- img.src = image;
- }
- });
-
- // 添加滚动监听
- scrollContainer.value = document.querySelector(".max-w-screen-2xl");
- if (scrollContainer.value) {
- window.addEventListener("scroll", handleScroll, { passive: true });
- }
- });
-
- // 清理滚动监听
- onUnmounted(() => {
- if (scrollContainer.value) {
- window.removeEventListener("scroll", handleScroll);
- }
- });
-
- // 处理滚动事件
- function handleScroll() {
- if (!scrollContainer.value) return;
-
- const containerRect = scrollContainer.value.getBoundingClientRect();
- const scrollTop = window.scrollY || document.documentElement.scrollTop;
-
- // 当容器顶部距离视窗顶部小于100px时,启用sticky
- isSticky.value = containerRect.top < 100;
- }
-
- // SEO优化
- useHead(() => ({
- title: `${product.value?.name} - Hanye`,
- meta: [
- {
- name: "description",
- content: product.value?.description,
- },
- ],
- }));
- </script>
-
- <style lang="scss" scoped>
- /* 隐藏滚动条但保持滚动功能 */
- .scrollbar-hide {
- -ms-overflow-style: none; /* IE and Edge */
- scrollbar-width: none; /* Firefox */
- }
- .scrollbar-hide::-webkit-scrollbar {
- display: none; /* Chrome, Safari and Opera */
- }
-
- /* 图片过渡动画 */
- .main-image {
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- }
-
- /* 缩略图悬停效果 */
- .thumbnail-item {
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- }
-
- .thumbnail-item:hover {
- transform: translateY(-2px);
- }
-
- /* 缩略图选中效果 */
- .thumbnail-item.selected {
- transform: scale(1.05);
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
- 0 2px 4px -1px rgba(0, 0, 0, 0.06);
- }
-
- /* 产品信息卡片效果 */
- .info-card {
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- }
-
- .info-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
- 0 2px 4px -1px rgba(0, 0, 0, 0.06);
- }
-
- /* 滚动跟随效果 */
- .lg\:sticky {
- position: sticky;
- top: 6rem; /* 96px */
- transition: all 0.3s ease;
- z-index: 10;
- max-height: calc(100vh - 6rem);
- overflow-y: auto;
- }
-
- @media (max-width: 1024px) {
- .lg\:sticky {
- position: relative;
- top: 0;
- max-height: none;
- }
- }
-
- /* Swiper 样式定制 */
- .product-swiper {
- :deep(.swiper-pagination-bullet) {
- background-color: white;
- opacity: 0.5;
- }
-
- :deep(.swiper-pagination-bullet-active) {
- opacity: 1;
- background-color: theme("colors.cyan.400");
- }
-
- :deep(.swiper-button-next),
- :deep(.swiper-button-prev) {
- color: theme("colors.cyan.500");
- background-color: rgba(0, 0, 0, 0.3);
- width: 36px;
- height: 36px;
- border-radius: 50%;
-
- &:after {
- font-size: 16px;
- font-weight: bold;
- }
-
- &:hover {
- background-color: theme("colors.cyan.500");
- color: white;
- }
- }
-
- :deep(.swiper-button-disabled) {
- opacity: 0.35;
- cursor: auto;
- pointer-events: none;
- }
-
- :deep(.swiper-slide) {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- }
-
- /* 缩略图Swiper样式 */
- .thumbs-swiper {
- overflow: visible;
- padding: 0.25rem;
- min-height: 6rem;
-
- :deep(.swiper-wrapper) {
- align-items: center;
- display: flex;
- min-height: 6rem;
- }
-
- :deep(.swiper-slide) {
- width: auto;
- height: auto;
- opacity: 0.7;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- :deep(.swiper-slide-thumb-active) {
- opacity: 1;
- transform: scale(1.05);
- z-index: 1;
- }
- }
-
- /* 自定义缩略图导航按钮 */
- .swiper-thumb-next,
- .swiper-thumb-prev {
- &:focus {
- outline: none;
- }
-
- &.swiper-button-disabled {
- opacity: 0.3;
- cursor: default;
- background-color: rgba(0, 0, 0, 0.2);
-
- &:hover {
- background-color: rgba(0, 0, 0, 0.2);
- }
- }
- }
-
- /* 响应式调整 */
- @media (max-width: 768px) {
- .thumbs-swiper {
- padding: 0;
-
- :deep(.swiper-slide) {
- margin-right: 8px;
- }
- }
-
- /* 确保缩略图容器在移动设备上有足够空间 */
- .product-thumbnail-container {
- padding: 1rem;
-
- .swiper-thumb-prev {
- left: 0.5rem;
- width: 2rem;
- height: 2rem;
- }
-
- .swiper-thumb-next {
- right: 0.5rem;
- width: 2rem;
- height: 2rem;
- }
- }
- }
-
- /* 解决不同分辨率下缩略图大小问题 */
- .thumbnail-fixed-size {
- width: 100%;
- height: 100%;
- aspect-ratio: 1 / 1;
- position: relative;
- }
-
- /* 表格样式和高亮效果 */
- :deep(.full-table-screen .prose table) {
- @apply border-collapse border border-zinc-700 rounded-lg overflow-hidden;
- }
-
- :deep(.full-table-screen .prose thead) {
- @apply bg-zinc-800;
- }
-
- :deep(.full-table-screen .prose th) {
- @apply px-6 py-3 text-left text-base font-medium text-zinc-300 uppercase tracking-wider border-b border-zinc-700;
- }
-
- :deep(.full-table-screen .prose tbody) {
- @apply bg-zinc-900 divide-y divide-zinc-700;
- }
-
- :deep(.full-table-screen .prose td) {
- @apply px-6 py-4 text-sm text-zinc-300 border-b border-zinc-700/50;
- }
-
- :deep(.full-table-screen .prose tr) {
- @apply transition-colors duration-200;
- }
-
- :deep(.full-table-screen .prose tr:hover:not(.highlighted-model-row)) {
- @apply bg-zinc-800/50;
- }
-
- /* 高亮当前型号对应的行 */
- :deep(.full-table-screen .prose tr.highlighted-model-row) {
- @apply bg-cyan-500/20 border border-cyan-400/50;
- }
-
- :deep(.full-table-screen .prose tr.highlighted-model-row td) {
- @apply text-cyan-400 font-medium;
- }
-
- /* 表格响应式处理 */
- @media (max-width: 640px) {
- :deep(.prose table) {
- font-size: 0.875rem;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- @apply px-3 py-2;
- }
- }
-
- /* 表格响应式容器 */
- :deep(.full-table-screen .prose) {
- /* 为包含表格的容器添加水平滚动 */
- & > *:has(table) {
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- scrollbar-width: thin;
- scrollbar-color: theme("colors.cyan.400") theme("colors.zinc.800");
- }
-
- /* 自定义滚动条样式 */
- & > *:has(table)::-webkit-scrollbar {
- height: 8px;
- }
-
- & > *:has(table)::-webkit-scrollbar-track {
- background: theme("colors.zinc.800");
- border-radius: 4px;
- }
-
- & > *:has(table)::-webkit-scrollbar-thumb {
- background: theme("colors.cyan.400");
- border-radius: 4px;
- }
-
- & > *:has(table)::-webkit-scrollbar-thumb:hover {
- background: theme("colors.cyan.300");
- }
- }
-
- /* 小屏幕表格优化 */
- @media (max-width: 768px) {
- :deep(.full-table-screen .prose table) {
- min-width: 100%;
- white-space: nowrap;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- @apply px-2 py-2 text-xs;
- min-width: 100px; /* 确保最小宽度 */
- }
-
- /* 高亮列在小屏幕上更明显 */
- :deep(.full-table-screen .prose td.highlighted-model-column),
- :deep(.full-table-screen .prose th.highlighted-model-column) {
- @apply bg-cyan-500/30;
- position: sticky;
- z-index: 10;
- }
-
- :deep(.full-table-screen .prose td.highlighted-model-column)::before,
- :deep(.full-table-screen .prose th.highlighted-model-column)::before {
- width: 4px;
- }
- }
-
- /* 超小屏幕处理 */
- @media (max-width: 480px) {
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- @apply px-1 py-1 text-xs;
- min-width: 80px;
- }
-
- /* 为高亮列添加固定位置,确保始终可见 */
- :deep(.full-table-screen .prose td.highlighted-model-column),
- :deep(.full-table-screen .prose th.highlighted-model-column) {
- position: sticky;
- left: 0;
- z-index: 20;
- box-shadow: 2px 0 4px rgba(0, 0, 0, 0.1);
- }
- }
-
- /* 列高亮基础样式 */
- :deep(.full-table-screen .prose td.highlighted-model-column),
- :deep(.full-table-screen .prose th.highlighted-model-column) {
- @apply bg-cyan-500/20 text-cyan-400 font-medium;
- position: relative;
- }
-
- :deep(.full-table-screen .prose td.highlighted-model-column)::before,
- :deep(.full-table-screen .prose th.highlighted-model-column)::before {
- content: "";
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 3px;
- background-color: theme("colors.cyan.400");
- }
-
- /* 表格行悬停效果 */
- :deep(.full-table-screen .prose tr:hover td:not(.highlighted-model-column)) {
- @apply bg-zinc-800/30;
- }
-
- /* 表格响应式包装器样式 */
- :deep(.table-responsive-wrapper) {
- border-radius: 8px;
- background: theme("colors.zinc.900");
- padding: 1rem 0;
- }
-
- :deep(.scroll-indicator) {
- animation: fadeInOut 3s ease-in-out infinite;
- }
-
- @keyframes fadeInOut {
- 0%,
- 100% {
- opacity: 0.6;
- }
- 50% {
- opacity: 1;
- }
- }
-
- /* 优化表格在响应式包装器中的显示 */
- :deep(.table-responsive-wrapper table) {
- margin: 0;
- border-radius: 0;
- }
-
- /* 滚动条增强 */
- :deep(.table-responsive-wrapper::-webkit-scrollbar) {
- height: 12px;
- }
-
- :deep(.table-responsive-wrapper::-webkit-scrollbar-track) {
- background: theme("colors.zinc.800");
- border-radius: 6px;
- margin: 0 8px;
- }
-
- :deep(.table-responsive-wrapper::-webkit-scrollbar-thumb) {
- background: linear-gradient(
- 45deg,
- theme("colors.cyan.500"),
- theme("colors.cyan.400")
- );
- border-radius: 6px;
- border: 2px solid theme("colors.zinc.800");
- }
-
- :deep(.table-responsive-wrapper::-webkit-scrollbar-thumb:hover) {
- background: linear-gradient(
- 45deg,
- theme("colors.cyan.400"),
- theme("colors.cyan.300")
- );
- }
-
- /* 简化的表格响应式处理 - 覆盖之前的复杂样式 */
- :deep(.full-table-screen .prose table) {
- min-width: 700px !important;
- width: 100% !important;
- white-space: nowrap;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- white-space: nowrap !important;
- min-width: 100px;
- }
-
- /* 移动设备优化 */
- @media (max-width: 768px) {
- :deep(.full-table-screen .prose table) {
- min-width: 800px !important;
- font-size: 0.875rem;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- min-width: 80px;
- padding: 0.5rem 0.75rem !important;
- }
- }
-
- @media (max-width: 480px) {
- :deep(.full-table-screen .prose table) {
- min-width: 600px !important;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- min-width: 90px !important;
- padding: 0.25rem 0.375rem !important;
- font-size: 0.75rem !important;
- }
- }
-
- /* 优化单元格宽度 - 更合适的显示空间 */
- :deep(.full-table-screen .prose table) {
- min-width: 1000px !important; /* 增加整体表格宽度 */
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- min-width: 150px !important; /* 增加单元格最小宽度 */
- max-width: 300px; /* 设置最大宽度避免过宽 */
- padding: 0.75rem 1rem !important; /* 增加内边距 */
- }
-
- /* 第一列(通常是标题列)给更多宽度 */
- :deep(.full-table-screen .prose th:first-child),
- :deep(.full-table-screen .prose td:first-child) {
- min-width: 200px !important;
- }
-
- /* 针对不同屏幕尺寸的优化 */
- @media (max-width: 1024px) {
- :deep(.full-table-screen .prose table) {
- min-width: 900px !important;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- min-width: 130px !important;
- padding: 0.5rem 0.75rem !important;
- }
- }
-
- @media (max-width: 768px) {
- :deep(.full-table-screen .prose table) {
- min-width: 800px !important;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- min-width: 120px !important;
- padding: 0.5rem 0.5rem !important;
- font-size: 0.875rem !important;
- }
- }
-
- @media (max-width: 640px) {
- :deep(.full-table-screen .prose table) {
- min-width: 700px !important;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- min-width: 100px !important;
- padding: 0.375rem 0.5rem !important;
- font-size: 0.8rem !important;
- }
- }
-
- @media (max-width: 480px) {
- :deep(.full-table-screen .prose table) {
- min-width: 600px !important;
- }
-
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- min-width: 90px !important;
- padding: 0.25rem 0.375rem !important;
- font-size: 0.75rem !important;
- }
- }
-
- /* 单元格内容截断和悬停显示 */
- :deep(.full-table-screen .prose th),
- :deep(.full-table-screen .prose td) {
- position: relative;
- overflow: hidden !important;
- text-overflow: ellipsis !important;
- white-space: nowrap !important;
- }
-
- /* 悬停时显示完整内容 */
- :deep(.full-table-screen .prose th:hover),
- :deep(.full-table-screen .prose td:hover) {
- overflow: visible !important;
- white-space: normal !important;
- z-index: 100;
- background-color: theme("colors.zinc.800") !important;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
- 0 2px 4px -1px rgba(0, 0, 0, 0.06);
- border-radius: 4px;
- }
-
- /* 高亮列的悬停效果 */
- :deep(.full-table-screen .prose th.highlighted-model-column:hover),
- :deep(.full-table-screen .prose td.highlighted-model-column:hover) {
- background-color: theme("colors.cyan.600") !important;
- color: white !important;
- }
-
- /* 确保悬停时的文本不会被遮挡 */
- :deep(.full-table-screen .prose table) {
- position: relative;
- z-index: 1;
- }
-
- :deep(.full-table-screen .prose th:hover),
- :deep(.full-table-screen .prose td:hover) {
- position: relative;
- z-index: 101;
- }
-
- /* 通用清除浮动工具类 */
- .clearfix::before,
- .clearfix::after {
- content: "";
- display: table;
- }
-
- .clearfix::after {
- clear: both;
- }
-
- /* 简化的clearfix */
- .clear-float::after {
- content: "";
- display: block;
- clear: both;
- }
-
- /* 清除左浮动 */
- .clear-left {
- clear: left;
- }
-
- /* 清除右浮动 */
- .clear-right {
- clear: right;
- }
-
- /* 清除所有浮动 */
- .clear-both {
- clear: both;
- }
- </style>
|